/*
 * Copyright 2007 National Bioinformatics Network
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dgbgui.modeldocument;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Observable;
import java.util.Set;

import org.jgrapht.DirectedGraph;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;

import dgbgui.modeldocument.definition.CustomDefinition;
import dgbgui.modeldocument.definition.Definition;
import dgbgui.modeldocument.primitives.Concept;
import dgbgui.modeldocument.primitives.ConceptType;
import dgbgui.modeldocument.primitives.Layer;
import dgbgui.modeldocument.primitives.RelationType;

/**
 * @author bjnortier
 * A model-driven query is a compound query, that can contain multiple
 * queries and unambiguous paths (and a target) 
 */
public class ModelDrivenWorkspace extends Observable {
    
    
    public enum ObservableEvent {
        CLEARED,
        RECREATED,
        UPDATED,
        TARGET_SET,
        INCLUDE_ADDED, 
        INCLUDE_REMOVED,
    }
    
    public class ObservableData {
        
        public ObservableData(ObservableEvent event) {
            m_event = event;
        }
        
        public ObservableEvent getEvent() {
            return m_event;
        }
        
        ObservableEvent m_event;
        
    }
    
    /**
     * Clears the model-driven workspace
     */
    public void clear() {
        m_target = null;
        m_connectionGraph = new DefaultDirectedGraph<Concept,DefaultEdge>(DefaultEdge.class);
        m_vertexQueries = new HashMap<Concept,Query>();
        
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.CLEARED));
    }
    
    public void setModel(DGBModel model) {
        clear();
        
        // Set the model, and create null queries
        // for all the vertices (concepts)
        // Also create the connection graph
        m_model = model;
        if ( model != null ) {
            m_vertexQueries = new HashMap<Concept,Query>();
            Set<Concept> concepts = model.getConcepts();
            for ( Concept concept : concepts ) {
                m_vertexQueries.put(concept,null);
                m_connectionGraph.addVertex(concept);
            }
        }
            
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.RECREATED));
    }
    
    /**
     * @param concept The concept with a new query
     * @param query The new query
     */
    public void setQuery(Concept concept, Query query) {
        m_vertexQueries.put(concept, query);
        
        updateTargetQuery();
        
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.UPDATED));
    }
    
    public DGBModel getModel() {
        return m_model;
    }

    
    /**
     * @param concept
     * @return The query for the concept. Null if no query yet
     */
    public Query getQuery(Concept concept) {
        // The map must be complete after createClean
        assert(m_vertexQueries.containsKey(concept));
        return m_vertexQueries.get(concept);
    }
    
    /**
     * @return All the concepts in the model
     */
    public Set<Concept> getConcepts() {
        return m_model.getConcepts();
    }
    
    public boolean isInModel(Concept concept) {
        if ( m_model == null ) {
            return false;
        }
        for ( Concept c : m_model.getConcepts() ) {
            if (c == concept) {
                return true;
            }
        }
        return false;
    }

    public Set<Concept> getConnectedConcepts(Concept concept) {
        Set<Concept> targets = m_model.getSourceConcepts(concept);
        targets.addAll(m_model.getTargetConcepts(concept));
        return targets;
    }
    
    /**
     * @param target The new target of the workspace
     */
    public void setTarget(Concept target) {

        m_target = target;
        
        updateTargetQuery();
        
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.TARGET_SET));
    }
    
    
    public Concept getTarget() {
        return m_target;
    }
    
    /**
     * @param source
     * @param target
     * @return the dgbQL string from the association, which is constructed with a closure, set and select
     */
    private String getAssociatedQLString(Concept source, Concept target) {
        String relTypeStr;
        Layer layerInstance;
        if ( m_model.hasEdge(source, target)) {
            RelationType relType = m_model.getRelationType(source,target);
            layerInstance = m_model.getLayer(source, target);
            
            relTypeStr = relType.m_name;
            
        } else {
            // Must be a reverse relation
            assert(m_model.hasEdge(target, source));
            
            RelationType relType = m_model.getRelationType(target,source);
            layerInstance = m_model.getLayer(target, source);
            
            relTypeStr = "reverse(" + relType.m_name + ")";
        }
        
        // Get the instance identifier string
        String layerInstanceStr = layerInstance.getDGBQLForPrimitive();
        assert(layerInstanceStr != null); 
        
        return "set( closure(transitive, " + 
            layerInstanceStr + "," +  getInstanceQLString(source) + ", rt:" + relTypeStr + "))";
        
    }
    
    private String getInstanceQLString(Concept concept) {
        // If there are incoming edges, we use that, together 
        // with the relation type and layer instance of the edge
        // to create the closure query
        Set<Concept> sources = getIncludeSources(concept);
        ConceptType type = (ConceptType)ModelDocument.getInstance().getConceptronType(concept);
        String ql = "";
        if ( sources.size() == 0 ) {
            // If there is a filter on the instance, use the QL from the filter,
            // oterwise is all the instances
            Query query = m_vertexQueries.get(concept); 
            if ( query != null ) {
                ql =  query.getDefinition().getdgbQLString();
            }
            else {
                ql = "ct:" + type.m_name + ".*";
            }
        } else { 
        
            int count = 0;
            for ( Concept source : sources ) {
                if ( count != 0 ) {
                        ql += " AND ";
                }
                ql += getAssociatedQLString(source,concept);
                ++count;
            
            }
            // Prepend the select
            ql = "select( (ct:" + type.m_name + ".*)" + ql + ")";
        }
        return ql;
    }
    
    /**
     * Get the query for finding the target terms 
     */
    public Query getTargetQuery() {
        if ( m_target == null ) {
            return null;
        }
        return m_vertexQueries.get(m_target);
    }
    
    /**
     * @param concept
     * @return true if the concept is on the fringe of the include graph
     */
    public boolean isOnFringe(Concept concept) {
        for ( DefaultEdge edge : m_connectionGraph.edgeSet() ) {
            if ( m_connectionGraph.getEdgeTarget(edge) == concept ) {
                return false;
            }
        }
        return true;
    }
    
    public Set<DefaultEdge> getIncludeEdges() {
        return m_connectionGraph.edgeSet();
    }
    
    
    public Concept getIncludeSource(DefaultEdge edge) {
        return m_connectionGraph.getEdgeSource(edge);
    }

    public Concept getIncludeTarget(DefaultEdge edge) {
        return m_connectionGraph.getEdgeTarget(edge);
    }

    private Set<Concept> getIncludeSources(Concept concept) {
        Set<Concept> sources = new HashSet<Concept>();
        for ( DefaultEdge edge :  m_connectionGraph.incomingEdgesOf(concept) ) {
            sources.add(m_connectionGraph.getEdgeSource(edge));
        }
        return sources;
    }

    public void addInclusion(Concept source, Concept target) {
        m_connectionGraph.addEdge(source, target);
        
        updateTargetQuery();
        
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.INCLUDE_ADDED));
    }
    
    public void removeInclusion(Concept source, Concept target) {
        m_connectionGraph.removeEdge(source, target);
        
        updateTargetQuery();
        
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.INCLUDE_REMOVED));
    }
    
    /** 
     * Update the target query according to the number of includes
     */
    private void updateTargetQuery() {
        
        
        
        if ( m_target != null ) {
            
            // Remove the current query from the Model Document
            // if there is one. The user could have deleted the
            // query from the model document, so only remove if it exists
            if ( m_vertexQueries.get(m_target) != null ) {
                if ( ModelDocument.getInstance().contains(m_vertexQueries.get(m_target))) {
                    ModelDocument.getInstance().remove(m_vertexQueries.get(m_target)); 
                }   
            }
            
            // If there is more than one include, construct and execute the
            // target query
            if ( getIncludeSources(m_target).size() > 0 ) {
                // New query for the target (a custom one for now) 
                Definition def = new CustomDefinition(getInstanceQLString(m_target));
                Query newQuery = new Query(def);

                ModelDocument.getInstance().add(newQuery);
                
                m_vertexQueries.put(m_target, newQuery);
                ResultStore.getInstance().submit(newQuery);
            } else {
                m_vertexQueries.put(m_target, null);
            }
        }
        
    }
    
    // ------------------- Serialization -------------
    
    /**
     * First serilization version
     */
    private static final long m_serialVersion = 1L;
    
    
    public void writeToStream(java.io.ObjectOutputStream stream) throws IOException {
        stream.writeLong(m_serialVersion);
        stream.writeObject(m_vertexQueries);
        stream.writeObject(m_model);
        stream.writeObject(m_target);
        stream.writeObject(m_connectionGraph);
    }
    
    
    @SuppressWarnings("unchecked")
    public void readFromStream(java.io.ObjectInputStream stream)  throws IOException, ClassNotFoundException {
        long x = stream.readLong();
        assert(x == m_serialVersion);
        m_vertexQueries = (Map<Concept,Query>)stream.readObject();
        m_model = (DGBModel)stream.readObject();
        m_target = (Concept)stream.readObject();
        m_connectionGraph = (DirectedGraph<Concept,DefaultEdge>)stream.readObject();
        
        setChanged();
        notifyObservers(new ObservableData(ObservableEvent.UPDATED));
        
        // If the result is availble, re-emit it so the results views are updated
        if ( ( m_target != null ) &&
             ( m_vertexQueries.get(m_target) != null ) && 
             ( ResultStore.getInstance().hasResult(m_vertexQueries.get(m_target))) ) {
            // Submitting the query again will notify the observers of
            // the result store, and since the query is stored already, it won't
            // be submitted to the back end
            ResultStore.getInstance().submit(m_vertexQueries.get(m_target));
        }
        
    }
    
    // ---- Singleton -----
    
    static public ModelDrivenWorkspace getInstance() {
        if ( m_instance == null ) {
            m_instance = new ModelDrivenWorkspace();
        }
        return m_instance;
    }
    
    private ModelDrivenWorkspace() {
        // Do nothing
    }
    
    static private ModelDrivenWorkspace m_instance;
    
    // ----- Fields -----
    
    private Map<Concept,Query> m_vertexQueries = new HashMap<Concept,Query>();
    
    // The model the workspace is based on
    private DGBModel m_model;

    // Thet target
    Concept m_target = null;
    
    // The connections
    DirectedGraph<Concept,DefaultEdge> m_connectionGraph = new DefaultDirectedGraph<Concept,DefaultEdge>(DefaultEdge.class);



}
